﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using System.Threading;
using System.Threading.Tasks;

namespace Raytracererer
{
    public class Renderer
    {
        Scene scene;

        internal Image ParalellRender(Raytracererer.Scene scene, RenderInfo renderInfo)
        {
            this.scene = scene;
            //skapa en bild och förbred den för skrivning
            Bitmap image = new Bitmap(renderInfo.width, renderInfo.height);
            System.Drawing.Imaging.BitmapData bitmapdata = image.LockBits(new Rectangle(0, 0, image.Width, image.Height), System.Drawing.Imaging.ImageLockMode.WriteOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
            IntPtr ptr = bitmapdata.Scan0;
            byte[] imagedata = new byte[4 * bitmapdata.Stride * image.Height];


            //beräkna x kordinaten av det virtuella plan som strålarna går igenom
            double x = renderInfo.width / (2 * Math.Tan(renderInfo.fov / 2));
            //lopa över planet och skicka en stråle per pixel
            int i = 0;

            Parallel.For(0, renderInfo.height, y =>
                {
                    for (int z = 0; z < renderInfo.width; z++)
                    {

                        //z + renderInfo.width / 2, y + renderInfo.height / 2
                        LinkedList<double> refractiveIndices = new LinkedList<double>();
                        refractiveIndices.AddFirst(scene.firstRefractiveIndex);
                        var color = sendRay(new Ray(scene.cam.position, new Vector3(x, y - renderInfo.height / 2, z - renderInfo.width / 2)), 1, refractiveIndices);
                        if (color.r > 1) color.r = 1;
                        if (color.g > 1) color.g = 1;
                        if (color.b > 1) color.b = 1;
                        imagedata[4 * (z + y * renderInfo.width)] = (byte)(color.b * 255);
                        imagedata[4 * (z + y * renderInfo.width) + 1] = (byte)(color.g * 255);
                        imagedata[4 * (z + y * renderInfo.width) + 2] = (byte)(color.r * 255);
                        imagedata[4 * (z + y * renderInfo.width) + 3] = 255;
                        //image.SetPixel(z,y,color);
                    }
                    i++;
                    Console.WriteLine("Done " + i + " of " + renderInfo.height + "  " + 100 * ((float)i / renderInfo.height) + "% om thread " + Thread.CurrentThread.ManagedThreadId);
                });

            //write the data to the image

            System.Runtime.InteropServices.Marshal.Copy(imagedata, 0, ptr, bitmapdata.Stride * image.Height);
            image.UnlockBits(bitmapdata);

            return image;
        }
        internal Image Render(Raytracererer.Scene scene, RenderInfo renderInfo)
        {
            this.scene = scene;
            //skapa en bild och förbred den för skrivning
            Bitmap image = new Bitmap(renderInfo.width, renderInfo.height);
            System.Drawing.Imaging.BitmapData bitmapdata = image.LockBits(new Rectangle(0, 0, image.Width, image.Height), System.Drawing.Imaging.ImageLockMode.WriteOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
            IntPtr ptr = bitmapdata.Scan0;
            byte[] imagedata = new byte[4 * bitmapdata.Stride * image.Height];


            //beräkna x kordinaten av det virtuella plan som strålarna går igenom
            double x = renderInfo.width / (2 * Math.Tan(renderInfo.fov / 2));
            //lopa över planet och skicka en stråle per pixel
            int i = 0;


            for (int z = 0; z < renderInfo.width; z++)
            {
                for (int y = 0; y < renderInfo.height; y++)
                {
                    //z + renderInfo.width / 2, y + renderInfo.height / 2
                    LinkedList<double> refractiveIndices = new LinkedList<double>();
                    refractiveIndices.AddFirst(scene.firstRefractiveIndex);
                    var color = sendRay(new Ray(scene.cam.position, new Vector3(x, y - renderInfo.height / 2, z - renderInfo.width / 2)), 1, refractiveIndices);
                    if (color.r > 1) color.r = 1;
                    if (color.g > 1) color.g = 1;
                    if (color.b > 1) color.b = 1;
                    imagedata[4 * (z + y * renderInfo.width)] = (byte)(color.b * 255);
                    imagedata[4 * (z + y * renderInfo.width) + 1] = (byte)(color.g * 255);
                    imagedata[4 * (z + y * renderInfo.width) + 2] = (byte)(color.r * 255);
                    imagedata[4 * (z + y * renderInfo.width) + 3] = 255;
                    //image.SetPixel(z,y,color);
                }
                Console.WriteLine("Finished column: " + z);
            }

            //write the data to the image

            System.Runtime.InteropServices.Marshal.Copy(imagedata, 0, ptr, bitmapdata.Stride * image.Height);
            image.UnlockBits(bitmapdata);

            return image;
        }


        internal bool ClosestIntersection(Ray ray, out Vector3 hitPosition,
            ref double minDist, out Primitive hitItem)
        {
            // minDist = double.MaxValue;
            Vector3 tmpposition;
            hitPosition = null;
            hitItem = null;
            bool returnValue = false;

            foreach (var item in scene.primitives)
            {
                var dist = item.Intersect(ray, out tmpposition);
                if (dist < 0) continue;
                if (dist < minDist)
                {
                    minDist = dist;
                    hitItem = item;
                    hitPosition = tmpposition;
                    returnValue = true;
                }
            }
            return returnValue;
        }


        internal Color sendRay(Ray ray, double power, LinkedList<double> refractiveIndices)
        {
            if (power < 0.01) return scene.backgroundcolor;

            double minDist = double.MaxValue;
            Primitive hitItem = null;
            Vector3 hitPosition = null;
            ClosestIntersection(ray, out hitPosition, ref minDist, out hitItem);

            if (hitItem == null) return scene.backgroundcolor;

            var hitItemNormal = hitItem.Normal(hitPosition);

            Color baseColor = hitItem.ColorAt(hitPosition);
            Color diffuseLight = scene.diffuseLight; // new Color(.5);// ambientLight;

            Color specularLight = scene.specularLight; // new Color(0, 0, 0);
            foreach (var light in scene.lights)
            {
                var towardsLight = light.position - hitPosition;
                var dist = (towardsLight).Length();
                var towardsLightNormalized = towardsLight / dist;

                Vector3 tmppos;
                Primitive tmpitem;

                if (!ClosestIntersection(new Ray(hitPosition - ray.Direction * 0.001, towardsLight), out tmppos, ref dist, out tmpitem))
                {//slut på skug test. se bara till att ta bort motsvarande slut } för ifsatsen bara


                    //gör beräkningar som behövs för varje lampa
                    if (towardsLightNormalized * hitItemNormal > 0)
                    {
                        diffuseLight += towardsLightNormalized * hitItemNormal * (light.color * (1.0 / (dist * dist)));
                    }
                    if (hitItemNormal * ((towardsLightNormalized + (light.position - ray.Position).Normalized()).Normalized()) > 0)
                        specularLight += (
                                Math.Pow(
                                    hitItemNormal *
                                    ((towardsLightNormalized + (light.position - ray.Position).Normalized()).Normalized())
                                    , 20)
                                ) * (light.color * (1.0 / (dist * dist)));
                }

            }



            //reflection
            Color colorFromreflected;
            colorFromreflected = sendRay(new Ray(hitPosition - ray.Direction * 0.001, ray.Direction - 2 * (hitItemNormal * ray.Direction) * hitItemNormal), power * hitItem.reflectivity, new LinkedList<double>(refractiveIndices));



            //refraktion
            Color colorFromRefracted = new Color(0, 0, 0);
            if (power * hitItem.transparency > 0.01)
            {
                bool entering = hitItemNormal * ray.Direction > 0 ? false : true;
                //Vector3 riktningen;
                double n1Overn2;


                if (entering)
                {
                    n1Overn2 = (refractiveIndices.Last.Value / hitItem.refractionIndex);
                    refractiveIndices.AddLast(hitItem.refractionIndex);
                }
                else
                {
                    n1Overn2 = (hitItem.refractionIndex / refractiveIndices.Last.Previous.Value);
                }
                //Console.WriteLine(n1Overn2);

                //double cosV1 = hitItemNormal * (-1f * ray.Direction);
                //double cosV2 = Math.Sqrt(1f - n1Overn2 * n1Overn2 * (1f - cosV1 * cosV1));
                //if (cosV1 > 0)
                //    riktningen = n1Overn2 * ray.Direction + ((n1Overn2) * cosV1 - cosV2) * hitItemNormal;
                //else
                //    riktningen = n1Overn2 * ray.Direction - ((n1Overn2) * cosV1 - cosV2) * hitItemNormal;

                //colorFromRefracted = sendRay(new Ray(hitPosition + ray.Direction * 0.001, riktningen), power * hitItem.transparency, new LinkedList<double>(refractiveIndices));

                Vector3 refractionDirection;

                Vector3 surfaceTangent = (ray.Direction - (ray.Direction * hitItemNormal) * hitItemNormal).Normalized();
                double sinIncidentAngle = surfaceTangent * ray.Direction; //lite snyggare sätt att få fram sinus
                double sinRefractionAngle = n1Overn2 * sinIncidentAngle; //underförstått att n1 är första mediumet
                if (sinRefractionAngle < 1) //ska alltid vara större än 0 ändå
                {
                    double cosRefractionAngle = Math.Sign(ray.Direction * hitItemNormal) * (Math.Sqrt(1 - sinRefractionAngle * sinRefractionAngle)); //Math.Sign bör ta hand om normalriktningen
                    refractionDirection = cosRefractionAngle * hitItemNormal + sinRefractionAngle * surfaceTangent;
                    if (!entering) refractiveIndices.RemoveLast();
                    colorFromRefracted = sendRay(new Ray(hitPosition + ray.Direction * 0.001, refractionDirection), power * hitItem.transparency, new LinkedList<double>(refractiveIndices));
                }
                else colorFromRefracted = sendRay(new Ray(hitPosition - ray.Direction * 0.001, ray.Direction - 2 * (hitItemNormal * ray.Direction) * hitItemNormal), power * hitItem.transparency, new LinkedList<double>(refractiveIndices));//var den olösbar och vi fick totalreflektion
                //sänd speglad iställe
            }



            //return colorFromreflected;
            //kombinera lampresultaten och 
            return (baseColor * diffuseLight + specularLight) * (1 - hitItem.reflectivity)
                + colorFromreflected * hitItem.reflectivity
                + colorFromRefracted * hitItem.transparency;
        }









































        internal void sendLightRay(Ray ray, double power, Color col, LinkedList<double> refractiveIndices)
        {
            if (power < 0.01) return;

            double minDist = double.MaxValue;
            Primitive hitItem = null;
            Vector3 hitPosition = null;
            ClosestIntersection(ray, out hitPosition, ref minDist, out hitItem);

            if (hitItem == null) return;

            if (hitItem is TexturedSphere)
            {
                ((TexturedSphere)hitItem).setLight(hitPosition, col);
            }


            var hitItemNormal = hitItem.Normal(hitPosition);

            Color baseColor = hitItem.ColorAt(hitPosition);
            Color diffuseLight = scene.diffuseLight; //new Color(.5);// ambientLight;

            Color specularLight = scene.specularLight;

            //reflection
            sendLightRay(new Ray(hitPosition - ray.Direction * 0.001, ray.Direction - 2 * (hitItemNormal * ray.Direction) * hitItemNormal), power * hitItem.reflectivity, col * hitItem.reflectivity, new LinkedList<double>(refractiveIndices));



            //refraktion
            Color colorFromRefracted = new Color(0, 0, 0);
            if (power * hitItem.transparency > 0.01)
            {
                bool entering = hitItemNormal * ray.Direction > 0 ? false : true;
                //Vector3 riktningen;
                double n1Overn2;


                if (entering)
                {
                    n1Overn2 = (refractiveIndices.Last.Value / hitItem.refractionIndex);
                    refractiveIndices.AddLast(hitItem.refractionIndex);
                }
                else
                {
                    n1Overn2 = (hitItem.refractionIndex / refractiveIndices.Last.Previous.Value);
                }
                //Console.WriteLine(n1Overn2);

                //double cosV1 = hitItemNormal * (-1f * ray.Direction);
                //double cosV2 = Math.Sqrt(1f - n1Overn2 * n1Overn2 * (1f - cosV1 * cosV1));
                //if (cosV1 > 0)
                //    riktningen = n1Overn2 * ray.Direction + ((n1Overn2) * cosV1 - cosV2) * hitItemNormal;
                //else
                //    riktningen = n1Overn2 * ray.Direction - ((n1Overn2) * cosV1 - cosV2) * hitItemNormal;

                //colorFromRefracted = sendRay(new Ray(hitPosition + ray.Direction * 0.001, riktningen), power * hitItem.transparency, new LinkedList<double>(refractiveIndices));

                Vector3 refractionDirection;

                Vector3 surfaceTangent = (ray.Direction - (ray.Direction * hitItemNormal) * hitItemNormal).Normalized();
                double sinIncidentAngle = surfaceTangent * ray.Direction; //lite snyggare sätt att få fram sinus
                double sinRefractionAngle = n1Overn2 * sinIncidentAngle; //underförstått att n1 är första mediumet
                if (sinRefractionAngle < 1) //ska alltid vara större än 0 ändå
                {
                    double cosRefractionAngle = Math.Sign(ray.Direction * hitItemNormal) * (Math.Sqrt(1 - sinRefractionAngle * sinRefractionAngle)); //Math.Sign bör ta hand om normalriktningen
                    refractionDirection = cosRefractionAngle * hitItemNormal + sinRefractionAngle * surfaceTangent;
                    if (!entering) refractiveIndices.RemoveLast();
                    sendLightRay(new Ray(hitPosition + ray.Direction * 0.001, refractionDirection), power * hitItem.transparency, col * hitItem.transparency, new LinkedList<double>(refractiveIndices));
                }
                else sendLightRay(new Ray(hitPosition - ray.Direction * 0.001, ray.Direction - 2 * (hitItemNormal * ray.Direction) * hitItemNormal), power * hitItem.transparency, col * hitItem.transparency, new LinkedList<double>(refractiveIndices));//var den olösbar och vi fick totalreflektion
                //sänd speglad iställe
            }



            //return colorFromreflected;
            //kombinera lampresultaten och 
            return;
            /*return (baseColor * diffuseLight + specularLight) * (1 - hitItem.reflectivity)
                + colorFromreflected * hitItem.reflectivity
                + colorFromRefracted * hitItem.transparency;*/
        }
    }
}
